أطلق العنان لمعالجة الفيديو المتقدمة في المتصفح. تعلم كيفية الوصول المباشر إلى بيانات مستويات إطار الفيديو (VideoFrame) الخام ومعالجتها باستخدام WebCodecs API لتأثيرات وتحليلات مخصصة.
الوصول إلى مستويات إطار الفيديو (VideoFrame) في WebCodecs: نظرة معمقة على معالجة بيانات الفيديو الخام
لسنوات، بدت معالجة الفيديو عالية الأداء في متصفح الويب وكأنها حلم بعيد المنال. كان المطورون غالبًا محصورين في قيود عنصر <video> وواجهة برمجة تطبيقات Canvas ثنائية الأبعاد (2D Canvas API)، والتي على الرغم من قوتها، إلا أنها كانت تسبب اختناقات في الأداء وتحد من الوصول إلى بيانات الفيديو الخام الأساسية. لقد غيّر وصول واجهة برمجة تطبيقات WebCodecs هذا المشهد بشكل جذري، موفرًا وصولًا منخفض المستوى إلى برامج ترميز الوسائط المدمجة في المتصفح. واحدة من أكثر ميزاتها ثورية هي القدرة على الوصول المباشر إلى البيانات الخام لإطارات الفيديو الفردية ومعالجتها من خلال كائن VideoFrame.
هذا المقال هو دليل شامل للمطورين الذين يتطلعون إلى تجاوز مجرد تشغيل الفيديو. سنستكشف تعقيدات الوصول إلى مستويات VideoFrame، ونزيل الغموض عن مفاهيم مثل فضاءات الألوان وتخطيط الذاكرة، ونقدم أمثلة عملية لتمكينك من بناء الجيل القادم من تطبيقات الفيديو داخل المتصفح، من الفلاتر في الوقت الفعلي إلى مهام رؤية الحاسوب المتطورة.
المتطلبات الأساسية
لتحقيق أقصى استفادة من هذا الدليل، يجب أن يكون لديك فهم قوي لما يلي:
- جافاسكريبت الحديثة: بما في ذلك البرمجة غير المتزامنة (
async/await, Promises). - مفاهيم الفيديو الأساسية: الإلمام بمصطلحات مثل الإطارات والدقة وبرامج الترميز مفيد.
- واجهات برمجة تطبيقات المتصفح: ستكون الخبرة مع واجهات برمجة التطبيقات مثل Canvas 2D أو WebGL مفيدة ولكنها ليست مطلوبة بشكل صارم.
فهم إطارات الفيديو وفضاءات الألوان والمستويات
قبل أن نتعمق في واجهة برمجة التطبيقات، يجب علينا أولاً بناء نموذج ذهني متين لما تبدو عليه بيانات إطار الفيديو بالفعل. الفيديو الرقمي هو سلسلة من الصور الثابتة، أو الإطارات. كل إطار عبارة عن شبكة من وحدات البكسل، ولكل بكسل لون. يتم تحديد كيفية تخزين هذا اللون بواسطة فضاء اللون وتنسيق البكسل.
RGBA: اللغة الأم للويب
معظم مطوري الويب على دراية بنموذج ألوان RGBA. يتم تمثيل كل بكسل بأربعة مكونات: الأحمر والأخضر والأزرق وألفا (الشفافية). عادةً ما يتم تخزين البيانات بشكل متداخل في الذاكرة، مما يعني أن قيم R و G و B و A لبكسل واحد يتم تخزينها بشكل متتالي:
[R1, G1, B1, A1, R2, G2, B2, A2, ...]
في هذا النموذج، يتم تخزين الصورة بأكملها في كتلة واحدة متصلة من الذاكرة. يمكننا التفكير في هذا على أنه وجود "مستوى" واحد من البيانات.
YUV: لغة ضغط الفيديو
ومع ذلك، نادرًا ما تعمل برامج ترميز الفيديو مع RGBA مباشرةً. فهي تفضل فضاءات الألوان YUV (أو بشكل أدق Y'CbCr). يفصل هذا النموذج معلومات الصورة إلى:
- Y (Luma): معلومات السطوع أو التدرج الرمادي. العين البشرية هي الأكثر حساسية للتغيرات في السطوع.
- U (Cb) and V (Cr): معلومات اللونية أو فرق اللون. العين البشرية أقل حساسية لتفاصيل اللون من تفاصيل السطوع.
هذا الفصل هو مفتاح الضغط الفعال. من خلال تقليل دقة مكونات U و V - وهي تقنية تسمى الاقتطاع اللوني (chroma subsampling) - يمكننا تقليل حجم الملف بشكل كبير مع أقل فقدان محسوس في الجودة. يؤدي هذا إلى تنسيقات بكسل مستوية (planar)، حيث يتم تخزين مكونات Y و U و V في كتل ذاكرة منفصلة، أو "مستويات".
أحد التنسيقات الشائعة هو I420 (نوع من YUV 4:2:0)، حيث يوجد لكل كتلة 2x2 من وحدات البكسل أربع عينات Y ولكن عينة U واحدة وعينة V واحدة فقط. هذا يعني أن مستويات U و V لها نصف عرض ونصف ارتفاع مستوى Y.
فهم هذا التمييز أمر حاسم لأن WebCodecs يمنحك وصولاً مباشرًا إلى هذه المستويات بالذات، تمامًا كما يوفرها مفكك الشفرة.
كائن VideoFrame: بوابتك إلى بيانات البكسل
القطعة المركزية في هذا اللغز هي كائن VideoFrame. إنه يمثل إطارًا واحدًا من الفيديو ولا يحتوي فقط على بيانات البكسل ولكن أيضًا على بيانات وصفية مهمة.
الخصائص الرئيسية لكائن VideoFrame
format: سلسلة نصية تشير إلى تنسيق البكسل (على سبيل المثال، 'I420', 'NV12', 'RGBA').codedWidth/codedHeight: الأبعاد الكاملة للإطار كما هي مخزنة في الذاكرة، بما في ذلك أي حشو يتطلبه برنامج الترميز.displayWidth/displayHeight: الأبعاد التي يجب استخدامها لعرض الإطار.timestamp: الطابع الزمني للعرض للإطار بالميكروثانية.duration: مدة الإطار بالميكروثانية.
الدالة السحرية: copyTo()
الدالة الأساسية للوصول إلى بيانات البكسل الخام هي videoFrame.copyTo(destination, options). تنسخ هذه الدالة غير المتزامنة بيانات مستوى الإطار إلى مخزن مؤقت (buffer) توفره.
destination: كائنArrayBufferأو مصفوفة ذات نوع (مثلUint8Array) كبيرة بما يكفي لاحتواء البيانات.options: كائن يحدد المستويات التي سيتم نسخها وتخطيطها في الذاكرة. إذا تم حذفه، فإنه ينسخ جميع المستويات في مخزن مؤقت واحد متجاور.
تعيد الدالة Promise يتم حله بمصفوفة من كائنات PlaneLayout، واحد لكل مستوى في الإطار. يحتوي كل كائن PlaneLayout على معلومتين حاسمتين:
offset: الإزاحة بالبايت حيث تبدأ بيانات هذا المستوى داخل المخزن المؤقت الوجهة.stride: عدد البايتات بين بداية صف واحد من وحدات البكسل وبداية الصف التالي لذلك المستوى.
مفهوم حاسم: Stride مقابل العرض
هذا هو أحد أكثر مصادر الالتباس شيوعًا للمطورين الجدد في برمجة الرسومات منخفضة المستوى. لا يمكنك افتراض أن كل صف من بيانات البكسل مكدس بإحكام واحد تلو الآخر.
- العرض (Width) هو عدد وحدات البكسل في صف من الصورة.
- الخطوة (Stride) (تسمى أيضًا pitch أو line step) هي عدد البايتات في الذاكرة من بداية صف واحد إلى بداية الصف التالي.
غالبًا، سيكون stride أكبر من width * bytes_per_pixel. هذا لأن الذاكرة غالبًا ما تكون محشوة لتتماشى مع حدود الأجهزة (على سبيل المثال، حدود 32 أو 64 بايت) لمعالجة أسرع بواسطة وحدة المعالجة المركزية (CPU) أو وحدة معالجة الرسومات (GPU). يجب عليك دائمًا استخدام الخطوة (stride) لحساب عنوان الذاكرة لبكسل في صف معين.
سيؤدي تجاهل الخطوة (stride) إلى صور مائلة أو مشوهة والوصول غير الصحيح إلى البيانات.
مثال عملي 1: الوصول إلى مستوى تدرج الرمادي وعرضه
لنبدأ بمثال بسيط ولكنه قوي. يتم ترميز معظم مقاطع الفيديو على الويب بتنسيق YUV مثل I420. مستوى 'Y' هو في الواقع تمثيل كامل للصورة بتدرج الرمادي. يمكننا استخراج هذا المستوى فقط وعرضه على لوحة رسم (canvas).
async function displayGrayscale(videoFrame) {
// نفترض أن إطار الفيديو بتنسيق YUV مثل 'I420' أو 'NV12'.
if (!videoFrame.format.startsWith('I4')) {
console.error('يتطلب هذا المثال تنسيقًا مستويًا YUV 4:2:0.');
videoFrame.close();
return;
}
const yPlaneInfo = videoFrame.layout[0]; // مستوى Y هو دائمًا الأول.
// إنشاء مخزن مؤقت للاحتفاظ ببيانات مستوى Y فقط.
const yPlaneData = new Uint8Array(yPlaneInfo.stride * videoFrame.codedHeight);
// نسخ مستوى Y إلى المخزن المؤقت الخاص بنا.
await videoFrame.copyTo(yPlaneData, {
rect: { x: 0, y: 0, width: videoFrame.codedWidth, height: videoFrame.codedHeight },
layout: [yPlaneInfo]
});
// الآن، يحتوي yPlaneData على وحدات البكسل الخام بتدرج الرمادي.
// نحتاج إلى عرضه. سنقوم بإنشاء مخزن مؤقت RGBA للوحة الرسم.
const canvas = document.getElementById('my-canvas');
canvas.width = videoFrame.displayWidth;
canvas.height = videoFrame.displayHeight;
const ctx = canvas.getContext('2d');
const imageData = ctx.createImageData(canvas.width, canvas.height);
// التكرار على بكسلات لوحة الرسم وتعبئتها من بيانات مستوى Y.
for (let y = 0; y < videoFrame.displayHeight; y++) {
for (let x = 0; x < videoFrame.displayWidth; x++) {
// هام: استخدم الخطوة (stride) للعثور على فهرس المصدر الصحيح!
const yIndex = y * yPlaneInfo.stride + x;
const luma = yPlaneData[yIndex];
// حساب فهرس الوجهة في المخزن المؤقت لبيانات الصورة RGBA.
const rgbaIndex = (y * canvas.width + x) * 4;
imageData.data[rgbaIndex] = luma; // أحمر
imageData.data[rgbaIndex + 1] = luma; // أخضر
imageData.data[rgbaIndex + 2] = luma; // أزرق
imageData.data[rgbaIndex + 3] = 255; // ألفا
}
}
ctx.putImageData(imageData, 0, 0);
// هام للغاية: أغلق VideoFrame دائمًا لتحرير ذاكرته.
videoFrame.close();
}
يسلط هذا المثال الضوء على عدة خطوات رئيسية: تحديد تخطيط المستوى الصحيح، وتخصيص مخزن مؤقت للوجهة، واستخدام copyTo لاستخراج البيانات، والتكرار الصحيح على البيانات باستخدام stride لإنشاء صورة جديدة.
مثال عملي 2: المعالجة في نفس المكان (فلتر بني داكن Sepia)
الآن دعونا نجري معالجة مباشرة للبيانات. فلتر البني الداكن (sepia) هو تأثير كلاسيكي يسهل تنفيذه. لهذا المثال، من الأسهل العمل مع إطار RGBA، والذي قد تحصل عليه من لوحة رسم (canvas) أو سياق WebGL.
async function applySepiaFilter(videoFrame) {
// يفترض هذا المثال أن الإطار المدخل هو 'RGBA' أو 'BGRA'.
if (videoFrame.format !== 'RGBA' && videoFrame.format !== 'BGRA') {
console.error('يتطلب مثال فلتر البني الداكن إطارًا بتنسيق RGBA.');
videoFrame.close();
return null;
}
// تخصيص مخزن مؤقت للاحتفاظ ببيانات البكسل.
const frameDataSize = videoFrame.allocationSize();
const frameData = new Uint8Array(frameDataSize);
await videoFrame.copyTo(frameData);
const layout = videoFrame.layout[0]; // RGBA هو مستوى واحد
// الآن، قم بمعالجة البيانات في المخزن المؤقت.
for (let y = 0; y < videoFrame.codedHeight; y++) {
for (let x = 0; x < videoFrame.codedWidth; x++) {
const pixelIndex = y * layout.stride + x * 4; // 4 بايت لكل بكسل (R,G,B,A)
const r = frameData[pixelIndex];
const g = frameData[pixelIndex + 1];
const b = frameData[pixelIndex + 2];
const tr = 0.393 * r + 0.769 * g + 0.189 * b;
const tg = 0.349 * r + 0.686 * g + 0.168 * b;
const tb = 0.272 * r + 0.534 * g + 0.131 * b;
frameData[pixelIndex] = Math.min(255, tr);
frameData[pixelIndex + 1] = Math.min(255, tg);
frameData[pixelIndex + 2] = Math.min(255, tb);
// قيمة ألفا (frameData[pixelIndex + 3]) تبقى دون تغيير.
}
}
// إنشاء إطار فيديو *جديد* بالبيانات المعدلة.
const newFrame = new VideoFrame(frameData, {
format: videoFrame.format,
codedWidth: videoFrame.codedWidth,
codedHeight: videoFrame.codedHeight,
timestamp: videoFrame.timestamp,
duration: videoFrame.duration
});
// لا تنس إغلاق الإطار الأصلي!
videoFrame.close();
return newFrame;
}
يوضح هذا دورة كاملة للقراءة والتعديل والكتابة: نسخ البيانات، والتكرار عليها باستخدام الخطوة (stride)، وتطبيق تحويل رياضي على كل بكسل، وإنشاء كائن VideoFrame جديد بالبيانات الناتجة. يمكن بعد ذلك عرض هذا الإطار الجديد على لوحة رسم، أو إرساله إلى VideoEncoder، أو تمريره إلى خطوة معالجة أخرى.
الأداء مهم: جافاسكريبت مقابل WebAssembly (WASM)
يمكن أن يكون التكرار على ملايين وحدات البكسل لكل إطار (يحتوي إطار 1080p على أكثر من 2 مليون بكسل، أو 8 ملايين نقطة بيانات في RGBA) في جافاسكريبت بطيئًا. على الرغم من أن محركات JS الحديثة سريعة بشكل لا يصدق، إلا أن هذا النهج يمكن أن يطغى بسهولة على الخيط الرئيسي للمعالجة في الوقت الفعلي للفيديو عالي الدقة (HD, 4K)، مما يؤدي إلى تجربة مستخدم متقطعة.
هنا يصبح WebAssembly (WASM) أداة أساسية. يسمح لك WASM بتشغيل التعليمات البرمجية المكتوبة بلغات مثل C++ أو Rust أو Go بسرعة شبه أصلية داخل المتصفح. يصبح سير العمل لمعالجة الفيديو كما يلي:
- في جافاسكريبت: استخدم
videoFrame.copyTo()للحصول على بيانات البكسل الخام فيArrayBuffer. - التمرير إلى WASM: قم بتمرير مرجع إلى هذا المخزن المؤقت إلى وحدة WASM المترجمة الخاصة بك. هذه عملية سريعة جدًا لأنها لا تتضمن نسخ البيانات.
- في WASM (C++/Rust): نفذ خوارزميات معالجة الصور المحسّنة للغاية مباشرة على مخزن الذاكرة المؤقت. هذا أسرع بأضعاف من حلقة جافاسكريبت.
- العودة إلى جافاسكريبت: بمجرد انتهاء WASM، يعود التحكم إلى جافاسكريبت. يمكنك بعد ذلك استخدام المخزن المؤقت المعدل لإنشاء
VideoFrameجديد.
لأي تطبيق جاد لمعالجة الفيديو في الوقت الفعلي - مثل الخلفيات الافتراضية، أو اكتشاف الكائنات، أو الفلاتر المعقدة - فإن الاستفادة من WebAssembly ليست مجرد خيار؛ إنها ضرورة.
التعامل مع تنسيقات البكسل المختلفة (مثل I420, NV12)
بينما يكون RGBA بسيطًا، ستتلقى في الغالب إطارات بتنسيقات YUV مستوية من VideoDecoder. دعونا نلقي نظرة على كيفية التعامل مع تنسيق مستوي بالكامل مثل I420.
سيكون لكائن VideoFrame بتنسيق I420 ثلاثة واصفات للتخطيط في مصفوفة layout الخاصة به:
layout[0]: مستوى Y (luma). الأبعاد هيcodedWidthxcodedHeight.layout[1]: مستوى U (chroma). الأبعاد هيcodedWidth/2xcodedHeight/2.layout[2]: مستوى V (chroma). الأبعاد هيcodedWidth/2xcodedHeight/2.
إليك كيف يمكنك نسخ جميع المستويات الثلاثة في مخزن مؤقت واحد:
async function extractI420Planes(videoFrame) {
const totalSize = videoFrame.allocationSize({ format: 'I420' });
const allPlanesData = new Uint8Array(totalSize);
const layouts = await videoFrame.copyTo(allPlanesData);
// layouts هي مصفوفة من 3 كائنات PlaneLayout
console.log('Y Plane Layout:', layouts[0]); // { offset: 0, stride: ... }
console.log('U Plane Layout:', layouts[1]); // { offset: ..., stride: ... }
console.log('V Plane Layout:', layouts[2]); // { offset: ..., stride: ... }
// يمكنك الآن الوصول إلى كل مستوى داخل المخزن المؤقت `allPlanesData`
// باستخدام الإزاحة والخطوة المحددة له.
const yPlaneView = new Uint8Array(
allPlanesData.buffer,
layouts[0].offset,
layouts[0].stride * videoFrame.codedHeight
);
// لاحظ أن أبعاد اللونية مقسومة على اثنين!
const uPlaneView = new Uint8Array(
allPlanesData.buffer,
layouts[1].offset,
layouts[1].stride * (videoFrame.codedHeight / 2)
);
const vPlaneView = new Uint8Array(
allPlanesData.buffer,
layouts[2].offset,
layouts[2].stride * (videoFrame.codedHeight / 2)
);
console.log('Accessed Y plane size:', yPlaneView.byteLength);
console.log('Accessed U plane size:', uPlaneView.byteLength);
videoFrame.close();
}
تنسيق شائع آخر هو NV12، وهو شبه مستوي. له مستويان: واحد لـ Y، ومستوى ثانٍ حيث تكون قيم U و V متداخلة (على سبيل المثال، [U1, V1, U2, V2, ...]). تتعامل واجهة برمجة تطبيقات WebCodecs مع هذا بشفافية؛ سيكون لكائن VideoFrame بتنسيق NV12 ببساطة تخطيطان في مصفوفة layout الخاصة به.
التحديات وأفضل الممارسات
العمل على هذا المستوى المنخفض قوي، ولكنه يأتي مع مسؤوليات.
إدارة الذاكرة أمر بالغ الأهمية
يحتفظ كائن VideoFrame بكمية كبيرة من الذاكرة، والتي غالبًا ما تتم إدارتها خارج كومة جامع القمامة في جافاسكريبت. إذا لم تقم بتحرير هذه الذاكرة بشكل صريح، فسوف تتسبب في تسرب للذاكرة يمكن أن يؤدي إلى انهيار علامة تبويب المتصفح.
دائمًا، ودائمًا قم باستدعاء videoFrame.close() عندما تنتهي من إطار ما.
الطبيعة غير المتزامنة
كل عمليات الوصول إلى البيانات غير متزامنة. يجب أن تتعامل بنية تطبيقك مع تدفق كائنات Promise وasync/await بشكل صحيح لتجنب حالات التسابق وضمان خط أنابيب معالجة سلس.
توافق المتصفح
WebCodecs هي واجهة برمجة تطبيقات حديثة. على الرغم من دعمها في جميع المتصفحات الرئيسية، تحقق دائمًا من توفرها وكن على دراية بأي تفاصيل تنفيذ أو قيود خاصة بالبائع. استخدم اكتشاف الميزات قبل محاولة استخدام واجهة برمجة التطبيقات.
الخاتمة: أفق جديد لفيديو الويب
إن القدرة على الوصول المباشر إلى بيانات المستوى الخام لكائن VideoFrame ومعالجتها عبر واجهة برمجة تطبيقات WebCodecs هي نقلة نوعية لتطبيقات الوسائط المستندة إلى الويب. إنها تزيل الصندوق الأسود لعنصر <video> وتمنح المطورين التحكم الدقيق الذي كان مخصصًا في السابق للتطبيقات الأصلية.
من خلال فهم أساسيات تخطيط ذاكرة الفيديو - المستويات، والخطوة (stride)، وتنسيقات الألوان - ومن خلال الاستفادة من قوة WebAssembly للعمليات التي تتطلب أداءً حرجًا، يمكنك الآن بناء أدوات معالجة فيديو متطورة بشكل لا يصدق مباشرة في المتصفح. من تصنيف الألوان في الوقت الفعلي والمؤثرات البصرية المخصصة إلى التعلم الآلي من جانب العميل وتحليل الفيديو، فإن الإمكانيات واسعة. لقد بدأ عصر الفيديو عالي الأداء ومنخفض المستوى على الويب حقًا.